﻿/****************************************************************************
* Copyright (c) 2008, j. montgomery, ATGi                                   *
* All rights reserved.                                                      *
*                                                                           *
* Redistribution and use in source and binary forms, with or without        *
* modification, are permitted provided that the following conditions        *
* are met:                                                                  *
*                                                                           *
* - Redistributions of source code must retain the above copyright          *
*   notice, this list of conditions and the following disclaimer.           *
*                                                                           *
* - Redistributions in binary form must reproduce the above                 *
*   copyright notice, this list of conditions and the following             *
*   disclaimer in the documentation and/or other materials provided         *
*   with the distribution.                                                  *
*                                                                           *
* - Neither the name of ATGi nor the names of its contributors may          *
*   be used to endorse or promote products derived from this software       *
*   without specific prior written permission.                              *
*                                                                           *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS       *
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT         *
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS         *
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE            *
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,       *
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES                  *
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR        *
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)        *
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,       *
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)             *
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED       *
* OF THE POSSIBILITY OF SUCH DAMAGE.                                        *
*****************************************************************************
*/
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using NetMail = System.Net.Mail;

using DnSmtp.Message;

namespace DnSmtp
{
    /// <summary>
    /// Handles the SMTP Communication (rfc 821)
    /// </summary>
    internal class SmtpHandler
    {
        #region Fields
        private NetMail.SmtpPermission _smtpPermissions;
        public enum ConnectionStates { Disconnected = 0, Connected = 1 };
        private ConnectionStates _connectedState;
        private int _connectionTimeout = 15;
        private const int HOST_PORT = 25;
        private TcpClient _tcpClient;
        private Stream _connectionStream;
        private string _messageCode;
        private string _messageStatus;
        private StringBuilder _smtpLog;
        private TraceLevel _traceLevel;
        private int quitCount = 0;
        #endregion Fields

        #region Properties
        /// <summary>
        /// Connection Timeoutn (in seconds)
        /// </summary>
        internal int ConnectionTimeout
        {
            get { return _connectionTimeout; }
            set { _connectionTimeout = value; }
        }

        /// <summary>
        /// 
        /// </summary>
        internal TraceLevel TraceLevel
        {
            get { return _traceLevel; }
            set { _traceLevel = value; }
        }

        /// <summary>
        /// The SmtpLog contains a copy of the converstaion between the SmtpHandler and the SMTP Server.
        /// </summary>
        internal string SmtpLog
        {
            get { return _smtpLog.ToString(); }
        }
        #endregion

        #region Constructor
        internal SmtpHandler()
        {
            _smtpPermissions = new NetMail.SmtpPermission(NetMail.SmtpAccess.Connect);

            _smtpLog = new StringBuilder();
            _traceLevel = TraceLevel.Verbose;
        }
        #endregion Constructor

        internal void Dispose()
        {
            this._tcpClient = null;
            this._connectionStream = null;
            this._smtpLog = null;
        }

        // Connect to the SMTP Server and send initial SMTP handshake (HELO).
        internal void Connect(string mailServerName, IPAddress sendingHostIp)
        {
            // Force a stack walk to make sure all callers have the SmtpPermission
            _smtpPermissions.Demand();

            // Set Connection Timeout
            _connectionTimeout = _connectionTimeout * 1000;

            IPAddress hostAddr = System.Net.Dns.GetHostEntry(mailServerName).AddressList[0];
            IPEndPoint epHost = new IPEndPoint(hostAddr, HOST_PORT);

            _tcpClient = new TcpClient();
            _tcpClient.ReceiveTimeout = ConnectionTimeout;
            _tcpClient.SendTimeout = ConnectionTimeout;
            _tcpClient.Connect(epHost);

            _connectionStream = _tcpClient.GetStream();
            _connectedState = ConnectionStates.Connected;
            // wait for server, e.g. '220 mail.domain.com ESMTP Sendmail 8.9.3/8.9.3; Mon, 10 Dec 2001 05:25:31 -0500'
            // TODO: Handle Timeout
            this.GetResponse();
            CheckResponseCode(_messageCode);

            // If no exception, then send HELO.
            if (sendingHostIp != null)
                this.SendCommand("HELO " + sendingHostIp.ToString());
            else
                this.SendCommand("HELO");
            // Environment.MachineName.ToLower() + "." + Environment.UserDomainName.ToLower() + ".com"
        }

        // Issues the SMTP QUIT command and then closes all open connections.
        internal void Quit()
        {
            this.SendCommand("QUIT");
        }

        public enum AuthType
        {
            PLAIN,
            //CRAM_MD5,
            //DIGEST_MD5,
            LOGIN
        }

        // TODO: NOT TESTED
        internal void Login(string username, string password, AuthType auth)
        {
            switch (auth) 
            {
                //case AuthType.CRAM_MD5:  //rfc2195
                //    throw new NotImplementedException();
                    // this.SendCommand("AUTH CRAM-MD5");
                    // break;
                //case AuthType.DIGEST_MD5:
                //    throw new NotImplementedException();
                    // this.SendCommand("AUTH DIGEST-MD5");
                    // break;
                case AuthType.PLAIN:
                    byte[] authBytes = ASCIIEncoding.ASCII.GetBytes(username + "\0" + username + "\0" + password);
                    string base64EncodedAuth = Convert.ToBase64String(authBytes);
                    
                    this.SendCommand("AUTH PLAIN" + base64EncodedAuth);
                    break;
                case AuthType.LOGIN:
                    byte[] authUser = ASCIIEncoding.ASCII.GetBytes(username);
                    string base64User = Convert.ToBase64String(authUser);

                    byte[] authPassword = ASCIIEncoding.ASCII.GetBytes(password);
                    string base64Password = Convert.ToBase64String(authPassword);

                    this.SendCommand("AUTH LOGIN");
                    this.SendCommand(base64User);
                    this.SendCommand(base64Password);

                    break;
            }
        }

        // Issues the SMTP QUIT command and then closes all open connections.
        internal void Disconnect()
        {
            if (_connectedState == ConnectionStates.Connected)
            {
                try
                {
                    if (quitCount++ > 3)
                        return;
                    this.SendCommand("QUIT");
                    _connectedState = ConnectionStates.Disconnected;
                    //reset on successful send
                    quitCount = 0;
                }
                catch (SmtpCommunicationException)
                {
                    //Problems sending the QUIT command itself should be limited.
                    quitCount++;
                }
                catch (Exception)
                {
                    // do nothing...in case this get's run more than once.
                }
                finally
                {
                    _connectionStream.Close();
                    _tcpClient.Close();
                }
            }
        }

        // This method gets the response from the SMTP server and checks the SMTP return code for an error.
        private void GetResponse()
        {
            if (_connectedState == ConnectionStates.Connected)
            {
                try
                {
                    // TODO: This really seems like it should be in a loop.
                    // What if more then 2048 bytes come from the mail server?
                    // Check the RFC to see if this is possible.
                    // Receive the page, loop until all bytes are received
                    Byte[] recvBytes = new Byte[2048];
                    int bytes = _connectionStream.Read(recvBytes, 0, recvBytes.Length);
                    string reply = ASCIIEncoding.ASCII.GetString(recvBytes, 0, bytes);
                    _smtpLog.Append(reply);

                    if (Regex.IsMatch(reply, "^[0-9][0-9][0-9]"))
                    {
                        _messageCode = reply.Substring(0, 3);
                        _messageStatus = reply.Substring(4, reply.Trim().Length - 4);
                    }
                    //					else 
                    //					{
                    //						while(!RegExpMatch(reply, "^[0-9][0-9][0-9] ")) 
                    //						{
                    //							bytes = _connectStream.Read(recvBytes, 0, recvBytes.Length);
                    //						}
                    //						_sMessageCode = reply.Substring(0,3);
                    //						_sMessageStatus = reply.Substring(4,reply.Length);
                    //					}
                }
                catch (System.IO.IOException ioe)
                {
                    _connectedState = ConnectionStates.Disconnected;
                    Trace.WriteLineIf(_traceLevel >= TraceLevel.Error, Thread.CurrentThread.Name + ": " + _smtpLog);
                    Trace.WriteLineIf(_traceLevel >= TraceLevel.Error, Thread.CurrentThread.Name + ": " + ioe.ToString());
                    throw new RecipientFailedException(ioe.Message, ioe);
                }
            }
        }

        // This method sends data to the SMTP server, then get's a response back.
        internal void SendCommand(string cmd)
        {
            if (_connectedState == ConnectionStates.Connected)
            {
                cmd = cmd + MailMessage.CRLF;
                Byte[] byteCmd = ASCIIEncoding.ASCII.GetBytes(cmd);

                _smtpLog.Append(cmd);
                _connectionStream.Write(byteCmd, 0, byteCmd.Length);

                // Every Send gets a response.
                this.GetResponse();
                this.CheckResponseCode(_messageCode);
            }
        }

        // This method contains all the response codes necessary from the SMTP server. Based on the type of response code,
        // we take differt types of action, do nothing, Disconnect, throw exceptions, etc.
        private void CheckResponseCode(string next)
        {
            string errorMessage = "SMTP Error " + _messageCode + ": " + _messageStatus;
            switch (Convert.ToInt32(next))
            {
                case 211:	// System status, or system help reply
                    break;
                case 214:	// Help message
                    //	[Information on how to use the receiver or the meaning of a
                    //	particular non-standard command; this reply is useful only
                    //	to the human user]
                    break;
                case 220:	// <domain> Service ready
                    break;
                case 221:	// <domain> Service closing transmission channel
                    break;
                case 235:   // AUTH SUCCESS
                    break;
                case 250:	// Requested mail action okay, completed
                    break;
                case 251:	// User not local; will forward to <forward-path>
                    break;
                case 334:   // Server Challenge / Ready Response  - RFC 2554
                    break;
                case 354:	// Start mail input; end with <CRLF>.<CRLF>
                    break;
                case 421:	// <domain> Service not available,
                    //	closing transmission channel
                    //	[This may be a reply to any command if the service knows it
                    //	must shut down]
                    _connectedState = ConnectionStates.Disconnected;
                    Disconnect();
                    throw new SmtpCommunicationException(errorMessage, _smtpLog.ToString());
                case 432:   // A password transistion is needed
                    break;
                case 450:	// Requested mail action not taken: mailbox unavailable
                    // [E.g., mailbox busy]
                    Disconnect();
                    throw new SmtpCommunicationException(errorMessage, _smtpLog.ToString());
                case 451:	// Requested action aborted: local error in processing
                    // This usually is thrown by qmail servers.
                    // See http://cr.yp.to/docs/smtplf.html
                    Disconnect();
                    throw new SmtpCommunicationException(errorMessage, _smtpLog.ToString());
                case 452:	// Requested action not taken: insufficient system storage
                    Disconnect();
                    throw new SmtpCommunicationException(errorMessage, _smtpLog.ToString());
                case 454:   // Temporary Authenticaion Failure
                    break;
                case 500:	// Syntax error, command unrecognized
                    // [This may include errors such as command line too long]
                    Disconnect();
                    throw new SmtpCommunicationException(errorMessage, _smtpLog.ToString());
                case 501:	// Syntax error in parameters or arguments
                    Disconnect();
                    throw new SmtpCommunicationException(errorMessage, _smtpLog.ToString());
                case 502:	// Command not implemented
                    Disconnect();
                    throw new SmtpCommunicationException(errorMessage, _smtpLog.ToString());
                case 503:	// Bad sequence of commands, e.g. 503 Need RCPT (recipient)
                    // Disconnect();
                    throw new RecipientFailedException(errorMessage);
                case 504:	// Command parameter not implemented
                    // NOTE: In cases of Authentication we shouldn't DISCONECT
                    Disconnect();
                    throw new SmtpCommunicationException(errorMessage, _smtpLog.ToString());
                case 530:   // Authentication REQUIRED
                    break;
                case 534:   // Authentication mechanism is too weak;
                    break;
                case 538:   // Encryption required for requested authenticaion mechanism
                    Disconnect();   // Encrypted connection not supported
                    break;
                case 550:	// Requested action not taken: mailbox unavailable
                    throw new RecipientFailedException(errorMessage);
                // [E.g., mailbox not found, no access]
                case 551:	// User not local; please try <forward-path>
                    throw new RecipientFailedException(errorMessage);
                case 552:	// Requested mail action aborted: exceeded storage allocation
                    throw new RecipientFailedException(errorMessage);
                case 553:	// Requested action not taken: mailbox name not allowed
                    //	[E.g., mailbox syntax incorrect]
                    throw new RecipientFailedException(errorMessage);
                case 554:	// Transaction failed
                    Disconnect();
                    throw new SmtpCommunicationException(errorMessage, _smtpLog.ToString());
                default:
                    Disconnect();
                    throw new SmtpCommunicationException(errorMessage, _smtpLog.ToString());
            }
        }
    }
}

